#!/usr/bin/python

# Copyright (c) 2018, Dag Wieers (dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
module: cobbler_system
short_description: Manage system objects in Cobbler
description:
  - Add, modify or remove systems in Cobbler.
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: full
options:
  host:
    description:
      - The name or IP address of the Cobbler system.
    default: 127.0.0.1
    type: str
  port:
    description:
      - Port number to be used for REST connection.
      - The default value depends on parameter O(use_ssl).
    type: int
  username:
    description:
      - The username to log in to Cobbler.
    default: cobbler
    type: str
  password:
    description:
      - The password to log in to Cobbler.
    type: str
  use_ssl:
    description:
      - If V(false), an HTTP connection is used instead of the default HTTPS connection.
    type: bool
    default: true
  validate_certs:
    description:
      - If V(false), SSL certificates are not validated.
      - This should only set to V(false) when used on personally controlled sites using self-signed certificates.
    type: bool
    default: true
  name:
    description:
      - The system name to manage.
    type: str
  properties:
    description:
      - A dictionary with system properties.
    type: dict
  interfaces:
    description:
      - A list of dictionaries containing interface options.
    type: dict
  sync:
    description:
      - Sync on changes.
      - Concurrently syncing Cobbler is bound to fail.
    type: bool
    default: false
  state:
    description:
      - Whether the system should be present, absent or a query is made.
    choices: [absent, present, query]
    default: present
    type: str
author:
  - Dag Wieers (@dagwieers)
notes:
  - Concurrently syncing Cobbler is bound to fail with weird errors.
"""

EXAMPLES = r"""
- name: Ensure the system exists in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: myhost
    properties:
      profile: CentOS6-x86_64
      name_servers: [2.3.4.5, 3.4.5.6]
      name_servers_search: foo.com, bar.com
    interfaces:
      eth0:
        macaddress: 00:01:02:03:04:05
        ipaddress: 1.2.3.4
  delegate_to: localhost

- name: Enable network boot in Cobbler
  community.general.cobbler_system:
    host: bdsol-aci-cobbler-01
    username: cobbler
    password: ins3965!
    name: bdsol-aci51-apic1.cisco.com
    properties:
      netboot_enabled: true
    state: present
  delegate_to: localhost

- name: Query all systems in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    state: query
  register: cobbler_systems
  delegate_to: localhost

- name: Query a specific system in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: '{{ inventory_hostname }}'
    state: query
  register: cobbler_properties
  delegate_to: localhost

- name: Ensure the system does not exist in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: myhost
    state: absent
  delegate_to: localhost
"""

RETURN = r"""
systems:
  description: List of systems.
  returned: O(state=query) and O(name) is not provided
  type: list
system:
  description: (Resulting) information about the system we are working with.
  returned: when O(name) is provided
  type: dict
"""

import ssl
import xmlrpc.client as xmlrpc_client

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.community.general.plugins.module_utils.datetime import (
    now,
)

IFPROPS_MAPPING = dict(
    bondingopts="bonding_opts",
    bridgeopts="bridge_opts",
    connected_mode="connected_mode",
    cnames="cnames",
    dhcptag="dhcp_tag",
    dnsname="dns_name",
    ifgateway="if_gateway",
    interfacetype="interface_type",
    interfacemaster="interface_master",
    ipaddress="ip_address",
    ipv6address="ipv6_address",
    ipv6defaultgateway="ipv6_default_gateway",
    ipv6mtu="ipv6_mtu",
    ipv6prefix="ipv6_prefix",
    ipv6secondaries="ipv6_secondariesu",
    ipv6staticroutes="ipv6_static_routes",
    macaddress="mac_address",
    management="management",
    mtu="mtu",
    netmask="netmask",
    static="static",
    staticroutes="static_routes",
    virtbridge="virt_bridge",
)


def getsystem(conn, name, token):
    system = dict()
    if name:
        # system = conn.get_system(name, token)
        systems = conn.find_system(dict(name=name), token)
        if systems:
            system = systems[0]
    return system


def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type="str", default="127.0.0.1"),
            port=dict(type="int"),
            username=dict(type="str", default="cobbler"),
            password=dict(type="str", no_log=True),
            use_ssl=dict(type="bool", default=True),
            validate_certs=dict(type="bool", default=True),
            name=dict(type="str"),
            interfaces=dict(type="dict"),
            properties=dict(type="dict"),
            sync=dict(type="bool", default=False),
            state=dict(type="str", default="present", choices=["absent", "present", "query"]),
        ),
        supports_check_mode=True,
    )

    username = module.params["username"]
    password = module.params["password"]
    port = module.params["port"]
    use_ssl = module.params["use_ssl"]
    validate_certs = module.params["validate_certs"]

    name = module.params["name"]
    state = module.params["state"]

    module.params["proto"] = "https" if use_ssl else "http"
    if not port:
        module.params["port"] = "443" if use_ssl else "80"

    result = dict(
        changed=False,
    )

    start = now()

    ssl_context = None if validate_certs or not use_ssl else ssl._create_unverified_context()

    url = "{proto}://{host}:{port}/cobbler_api".format(**module.params)
    conn = xmlrpc_client.ServerProxy(url, context=ssl_context)

    try:
        token = conn.login(username, password)
    except xmlrpc_client.Fault as e:
        module.fail_json(
            msg="Failed to log in to Cobbler '{url}' as '{username}'. {error}".format(
                url=url, error=f"{e}", **module.params
            )
        )
    except Exception as e:
        module.fail_json(msg=f"Connection to '{url}' failed. {e}")

    system = getsystem(conn, name, token)
    # result['system'] = system

    if state == "query":
        if name:
            result["system"] = system
        else:
            # Turn it into a dictionary of dictionaries
            # all_systems = conn.get_systems()
            # result['systems'] = { system['name']: system for system in all_systems }

            # Return a list of dictionaries
            result["systems"] = conn.get_systems()

    elif state == "present":
        if system:
            # Update existing entry
            system_id = ""
            # https://github.com/cobbler/cobbler/blame/v3.3.7/cobbler/api.py#L277
            if float(conn.version()) >= 3.4:
                system_id = conn.get_system_handle(name)
            else:
                system_id = conn.get_system_handle(name, token)

            for key, value in module.params["properties"].items():
                if key not in system:
                    module.warn(f"Property '{key}' is not a valid system property.")
                if system[key] != value:
                    try:
                        conn.modify_system(system_id, key, value, token)
                        result["changed"] = True
                    except Exception as e:
                        module.fail_json(msg=f"Unable to change '{key}' to '{value}'. {e}")

        else:
            # Create a new entry
            system_id = conn.new_system(token)
            conn.modify_system(system_id, "name", name, token)
            result["changed"] = True

            if module.params["properties"]:
                for key, value in module.params["properties"].items():
                    try:
                        conn.modify_system(system_id, key, value, token)
                    except Exception as e:
                        module.fail_json(msg=f"Unable to change '{key}' to '{value}'. {e}")

        # Add interface properties
        interface_properties = dict()
        if module.params["interfaces"]:
            for device, values in module.params["interfaces"].items():
                for key, value in values.items():
                    if key == "name":
                        continue
                    if key not in IFPROPS_MAPPING:
                        module.warn(f"Property '{key}' is not a valid system property.")
                    if not system or system["interfaces"][device][IFPROPS_MAPPING[key]] != value:
                        result["changed"] = True
                    interface_properties[f"{key}-{device}"] = value

            if result["changed"] is True:
                conn.modify_system(system_id, "modify_interface", interface_properties, token)

        # Only save when the entry was changed
        if not module.check_mode and result["changed"]:
            conn.save_system(system_id, token)

    elif state == "absent":
        if system:
            if not module.check_mode:
                conn.remove_system(name, token)
            result["changed"] = True

    if not module.check_mode and module.params["sync"] and result["changed"]:
        try:
            conn.sync(token)
        except Exception as e:
            module.fail_json(msg=f"Failed to sync Cobbler. {e}")

    if state in ("absent", "present"):
        result["system"] = getsystem(conn, name, token)

        if module._diff:
            result["diff"] = dict(before=system, after=result["system"])

    elapsed = now() - start
    module.exit_json(elapsed=elapsed.seconds, **result)


if __name__ == "__main__":
    main()
